콘텐츠로 이동

04. R 데이터 구조

R의 데이터 구조는 크게 "몇 차원인가"와 "원소가 모두 같은 타입이어야 하는가"라는 두 기준으로 나뉩니다. 벡터·행렬·배열은 같은 타입의 값만 담는 동질적(homogeneous) 구조이고, 차원 수만 1차원(벡터)·2차원(행렬)·N차원(배열)으로 다릅니다. 반면 리스트는 서로 다른 타입·구조를 마음대로 섞어 담을 수 있는 이질적(heterogeneous) 구조입니다. 데이터 프레임은 겉보기엔 행렬처럼 표 모양이지만, 실제로는 "길이가 같은 벡터들을 열로 묶은 리스트"입니다. 팩터는 "정해진 값(levels)만 허용하는 특수한 벡터"입니다.

구조 차원 원소 동질성 실제 기반 구조 대표 함수
벡터 Vector 1차원 동질(같은 타입) atomic vector c(), seq(), rep()
행렬 Matrix 2차원 동질 벡터 + dim 속성 matrix(), cbind(), rbind()
배열 Array N차원(N≥1) 동질 벡터 + dim 속성 array()
팩터 Factor 1차원 동질(범주형) 정수 벡터 + levels 속성 factor()
데이터 프레임 Data frame 2차원(표) 열마다 다른 타입 허용 리스트의 특수한 형태 data.frame()
리스트 List 임의 구조(중첩 가능) 이질(서로 다른 타입/구조 허용) 범용 컨테이너 list()

이 표에서 보듯 행렬과 배열은 사실 "벡터에 차원(dim) 속성이 추가로 붙은 것"이고, 데이터 프레임은 "리스트에 표 모양이라는 제약(모든 열의 길이가 같아야 함)이 추가로 붙은 것"입니다. 즉 R의 데이터 구조는 서로 무관한 6개의 개념이 아니라, 벡터와 리스트라는 두 뿌리에서 파생된 것이라 이해하면 전체 그림이 훨씬 단순해집니다.

4.1 벡터 Vector

벡터란 동일한 형태(예: 숫자 또는 문자)의 데이터 값들이 나열되어 있는 1차원 데이터 구조입니다. R에서 가장 기본이 되는 자료구조이며, 사실 R에서는 숫자 하나(5)조차 "길이가 1인 벡터"로 취급됩니다.

벡터 만들기

c() 함수로 벡터 만들기

c(...)는 여러 개의 값을 하나로 묶어(combine) 벡터를 만드는 함수입니다.

# 변수 v1에 숫자 1, 2, 3을 할당
v1 <- c(1, 2, 3)
v2 <- c(4, 5, 6)
v1
#> [1] 1 2 3

v2
#> [1] 4 5 6

c()는 벡터끼리도 이어 붙일 수 있습니다.

# 변수 v3에 변수 v1과 v2의 값들을 결합하여 할당
v3 <- c(v1, v2)
v3
#> [1] 1 2 3 4 5 6
# 문자열 벡터 만들기
s1 <- c("서울", "부산", "대구")
s1
#> [1] "서울" "부산" "대구"

연속적인 숫자로 벡터 만들기

start:end는 start부터 end까지 1씩 증감하는 정수 계열의 벡터를 만듭니다. start가 end보다 크면 거꾸로(내림차순) 생성됩니다.

1:5
#> [1] 1 2 3 4 5

5:1
#> [1] 5 4 3 2 1

seq(from, to, by = , length.out = )는 :보다 유연하게 등간격 수열을 만듭니다. 간격(by)이나 전체 개수(length.out)를 지정할 수 있습니다.

seq(1, 10, by = 2)
#> [1] 1 3 5 7 9

seq(1, 10, length.out = 5)
#> [1]  1.00  3.25  5.50  7.75 10.00

💡 1:length(x)의 함정과 seq_len()·seq_along()

for (i in 1:length(x))처럼 반복문의 범위를 1:length(x)로 쓰는 코드를 자주 보게 되지만, x가 빈 벡터(길이 0)일 때 length(x)는 0이 되어 1:0이 c(1, 0)으로 계산되는 함정이 있습니다. 의도는 "반복 없음"인데 실제로는 두 번 반복되어 버립니다.

empty <- numeric(0)
1:length(empty)      # 의도와 다르게 1, 0 두 값이 나옴
seq_along(empty)      # 빈 벡터일 때도 안전하게 길이 0
#> [1] 1 0
#> integer(0)

seq_len(n)은 1:n과, seq_along(x)는 1:length(x)와 각각 같은 결과를 내면서도 n이 0이거나 x가 빈 벡터일 때 안전하게 동작하므로, 반복문이나 함수 안에서 범위를 지정할 때는 :보다 seq_len()·seq_along()을 쓰는 것이 안전한 관행으로 자리 잡았습니다.

n <- 5
seq_len(n)
#> [1] 1 2 3 4 5

x <- c(10, 20, 30)
seq_along(x)
#> [1] 1 2 3

rep(x, times = , each = )는 값을 반복해 벡터를 만듭니다. times는 전체(혹은 각 값)를 반복할 횟수, each는 각 값을 연속으로 반복할 횟수입니다.

rep(5, times = 3)          # 5를 3번 반복
#> [1] 5 5 5

rep(c("a", "b"), each = 2) # 각 값을 2번씩 반복
#> [1] "a" "a" "b" "b"

times에 벡터를 넘기면 각 값마다 반복 횟수를 다르게 지정할 수 있습니다.

# 1은 1번, 2는 2번, 3은 3번 반복
rep(1:3, times = c(1, 2, 3))
#> [1] 1 2 2 3 3 3

벡터 요소 값에 이름 부여

names(x) <- ...로 나중에 이름을 붙일 수도 있고, c()로 만들 때 이름 = 값 형태로 바로 이름을 지정할 수도 있습니다.

# names() 함수를 이용해 값마다 이름 부여
mscore <- c(95, 85, 75)
names(mscore) <- c("김철수", "이영희", "홍길동")
mscore
#> 김철수 이영희 홍길동 
#>     95     85     75
# 만들 때 바로 이름 지정
scores <- c(김민준 = 88, 이서연 = 95, 박도윤 = 76)
scores
#> 김민준 이서연 박도윤 
#>     88     95     76 

names(scores)
#> [1] "김민준" "이서연" "박도윤"

벡터 요소 추출

벡터 안에 들어있는 개별 값들을 요소(element)라고 부릅니다. 요소 추출은 대괄호 [ ] 안에 "몇 번째(위치)", "무슨 이름", "몇 번째만 빼고(음수)", "어떤 조건을 만족하는" 값인지를 지정하는 네 가지 방식으로 이루어집니다.

특정 위치에 있는 요소 추출

v <- c(11, 12, 13, 14, 15)
v[1]
#> [1] 11

v[2:3]
#> [1] 12 13

v[c(1, 3, 5)]
#> [1] 11 13 15

이름으로 요소 추출

scores <- c(김민준 = 88, 이서연 = 95, 박도윤 = 76)
scores["이서연"]
#> 이서연 
#>     95 

마이너스 기호(-)를 이용하여 요소 제외

v[-2]
#> [1] 11 13 14 15

v[-c(1, 2)]
#> [1] 13 14 15

조건으로 요소 추출

논리 벡터를 [ ] 안에 넣으면 TRUE인 위치의 값만 골라냅니다.

s <- c(3, 1, 5, 9, 7, 10)

# 5 이상인 값만 골라내 부분 벡터로 추출
s[s >= 5]
#> [1]  5  9  7 10

which(조건)은 값 자체가 아니라 조건을 만족하는 위치(인덱스)를 돌려줍니다.

# 8보다 큰 값의 위치(인덱스) 반환
which(s >= 8)
#> [1] 4 6

%in%은 "왼쪽 값이 오른쪽 벡터 안에 있는지"를 원소별로 검사해 논리 벡터를 돌려줍니다. ==와 달리 비교 대상이 여러 개일 때 특히 유용합니다.

# s의 각 값이 1, 5, 10 중 하나인지 검사
s %in% c(1, 5, 10)
#> [1] FALSE  TRUE  TRUE FALSE FALSE  TRUE

s[s %in% c(1, 5, 10)]
#> [1]  1  5 10

subset(x, 조건)도 조건 추출을 할 수 있습니다.

subset(s, s >= 8)
#> [1]  9 10

⚠️ subset() 사용 시 주의: subset()은 조건식에 변수명을 따옴표 없이 바로 쓸 수 있어 대화형으로 빠르게 살펴볼 때는 편리하지만, R 도움말(?subset)에도 명시되어 있듯이 이런 비표준 평가(non-standard evaluation) 방식 때문에 함수나 스크립트 안에서 프로그래밍적으로 사용하기에는 안전하지 않은 경우가 있습니다. 코드에 직접 쓰는 대화형 분석에는 무방하지만, 재사용 가능한 함수를 작성할 때는 x[조건, ] 형태의 표준 인덱싱을 쓰는 것이 안전합니다.

벡터 연산

벡터끼리 사칙연산을 하면 같은 위치의 값끼리 계산되는 원소별(element-wise) 연산이 이루어집니다.

v <- c(11, 12, 13, 14, 15)
w <- c(1, 2, 3, 4, 5)

v + w         # 11+1, 12+2, ...
#> [1] 12 14 16 18 20

v * w         # 11*1, 12*2, ...
#> [1] 11 24 39 56 75

w + 2         # 1+2, 2+2, ... (짧은 벡터는 재사용됨)
#> [1] 3 4 5 6 7

sqrt(w)       # sqrt(1), sqrt(2), ...
#> [1] 1.000000 1.414214 1.732051 2.000000 2.236068

sum(w)        # 1+2+3+4+5
#> [1] 15

길이가 다른 벡터끼리 연산하면 짧은 쪽이 길이에 맞춰 반복되는데, 이를 재사용 규칙(recycling rule)이라고 합니다. 위 예제의 w + 2도 사실 길이 1인 2가 5번 재사용된 것입니다. 다만 긴 벡터의 길이가 짧은 벡터 길이의 배수가 아니면 의도치 않은 계산일 가능성이 높으므로 R이 경고를 냅니다.

c(1, 2, 3, 4) + c(1, 2, 3)
#> [1] 2 4 6 5
#> 경고메시지(들):
#> c(1, 2, 3, 4) + c(1, 2, 3)에서:
#>   두 객체의 길이가 서로 배수관계에 있지 않습니다

기타 (결측값 처리 등)

벡터 길이 측정

length(v)
#> [1] 5

벡터 결측값 처리

xx <- c(1, NA, 3, NA, 5)

# 결측값 유무 검사
is.na(xx)
#> [1] FALSE  TRUE FALSE  TRUE FALSE
# 결측값 개수 세기 (TRUE는 1로 계산됨)
sum(is.na(xx))
#> [1] 2

na.omit(x)은 결측값을 제외한 나머지 값만 돌려줍니다.

na.omit(xx)
#> [1] 1 3 5
#> attr(,"na.action")
#> [1] 2 4
#> attr(,"class")
#> [1] "omit"

4.2 행렬 Matrix

행렬(matrix)은 "같은 타입의 값을 가진 벡터에 2차원(행 수 × 열 수) 구조를 부여한 것"입니다. 벡터와 마찬가지로 모든 원소는 같은 타입(숫자면 숫자, 문자면 문자)이어야 합니다. 내부적으로는 1차원 벡터에 dim 속성(행 수, 열 수)이 추가된 것입니다.

행렬 생성

matrix()를 사용하여 행렬 생성

matrix(data, nrow, ncol, byrow = FALSE)는 벡터 데이터를 행렬 형태로 채웁니다. byrow가 FALSE(기본값)이면 열 방향으로, TRUE이면 행 방향으로 채워집니다.

# 1부터 12까지의 숫자를 4행 3열 행렬로 채우기 (열 우선)
# nrow : 행의 수, ncol : 열의 수
m1 <- matrix(1:12, nrow = 4, ncol = 3)
m1
#>      [,1] [,2] [,3]
#> [1,]    1    5    9
#> [2,]    2    6   10
#> [3,]    3    7   11
#> [4,]    4    8   12
# byrow = TRUE : 행렬이 행 방향으로 채워짐
m2 <- matrix(c(1, 2, 3, 11, 12, 13), nrow = 2, byrow = TRUE)
m2
#>      [,1] [,2] [,3]
#> [1,]    1    2    3
#> [2,]   11   12   13

cbind() 또는 rbind()를 사용하여 행렬 생성

cbind()는 벡터를 열(column)로, rbind()는 벡터를 행(row)으로 묶어 행렬을 만듭니다.

col1 <- c(5, 6, 7, 8, 9)
col2 <- c(2, 4, 5, 9, 8)
col3 <- c(7, 3, 4, 8, 7)

# 열 기준으로 벡터 병합
m3 <- cbind(col1, col2, col3)
m3
#>      col1 col2 col3
#> [1,]    5    2    7
#> [2,]    6    4    3
#> [3,]    7    5    4
#> [4,]    8    9    8
#> [5,]    9    8    7

행렬 이름 붙이기

rownames() 또는 colnames()를 사용하여 이름 붙이기

rownames(m3) <- paste0("행", 1:5)
colnames(m3) <- c("열1", "열2", "열3")
m3
#>     열1 열2 열3
#> 행1   5   2   7
#> 행2   6   4   3
#> 행3   7   5   4
#> 행4   8   9   8
#> 행5   9   8   7

matrix()의 dimnames 속성을 이용하여 이름 붙이기

행렬을 만드는 시점에 dimnames = list(행이름벡터, 열이름벡터)를 지정하면 한 번에 이름까지 붙일 수 있습니다.

m4 <- matrix(1:6, nrow = 2, byrow = TRUE,
             dimnames = list(c("행1", "행2"), c("일열", "이열", "삼열")))
m4
#>     일열 이열 삼열
#> 행1    1    2    3
#> 행2    4    5    6

행렬 바꾸기·차원

행렬 전치(t)

t(x)는 행과 열을 서로 뒤바꾼(transpose) 행렬을 돌려줍니다.

t(m2)
#>      [,1] [,2]
#> [1,]    1   11
#> [2,]    2   12
#> [3,]    3   13

행렬의 차원

nrow(m3)   # 행의 개수
#> [1] 5

ncol(m3)   # 열의 개수
#> [1] 3

dim(m3)    # 행과 열의 개수 (행, 열)
#> [1] 5 3

💡 R 4.0.0부터 달라진 점 — 행렬의 클래스: R 4.0.0 이전에는 class(matrix(...))가 "matrix" 한 가지만 돌려주었지만, R 4.0.0부터는 "matrix"와 "array" 두 클래스를 함께 갖도록 바뀌었습니다. 예전 코드에서 class(x) == "matrix"처럼 클래스를 문자열 하나와 직접 비교하던 부분이 있다면, R 4.0.0 이상에서는 의도와 다르게 동작할 수 있으므로 is.matrix(x)를 쓰는 것이 더 안전합니다.

class(m1)
is.matrix(m1)
is.array(m1)
#> [1] "matrix" "array" 
#> [1] TRUE
#> [1] TRUE

행렬의 요소 추출·수정

특정 위치에 있는 요소 추출

# 2번째 행
m3[2, ]
#> 열1 열2 열3 
#>   6   4   3 

# 2행 3열
m3[2, 3]
#> [1] 3

# 2행부터 3행
m3[2:3, ]
#>     열1 열2 열3
#> 행2   6   4   3
#> 행3   7   5   4

# 2행과 5행
m3[c(2, 5), ]
#>     열1 열2 열3
#> 행2   6   4   3
#> 행5   9   8   7

행렬 이름으로 요소 추출

# 두번째 열
m3[, "열2"]
#> 행1 행2 행3 행4 행5 
#>   2   4   5   9   8 

# 세번째 행
m3["행3", ]
#> 열1 열2 열3 
#>   7   5   4 

마이너스 기호(-)를 이용하여 요소 제외

# 2행과 2열 제외
m3[-2, -2]
#>     열1 열3
#> 행1   5   7
#> 행3   7   4
#> 행4   8   8
#> 행5   9   7

조건으로 요소 추출

# 5보다 큰 수 모두 추출
m3[m3 > 5]
#> [1] 6 7 8 9 9 8 7 8 7

# 짝수만 추출
m3[m3 %% 2 == 0]
#> [1] 6 8 2 4 8 4 8

행렬 요소 수정

mm <- m3
# 하나의 요소 값 수정
mm[2, 2] <- 9
mm
#>     열1 열2 열3
#> 행1   5   2   7
#> 행2   6   9   3
#> 행3   7   5   4
#> 행4   8   9   8
#> 행5   9   8   7
# 6 미만인 값을 모두 0으로 일괄 수정
mm[mm < 6] <- 0
mm
#>     열1 열2 열3
#> 행1   0   0   7
#> 행2   6   9   0
#> 행3   7   0   0
#> 행4   8   9   8
#> 행5   9   8   7

행렬 연산

# 스칼라 곱
m1 * 2
#>      [,1] [,2] [,3]
#> [1,]    2   10   18
#> [2,]    4   12   20
#> [3,]    6   14   22
#> [4,]    8   16   24
# 행 합계, 열 합계
rowSums(m3)
#> 행1 행2 행3 행4 행5 
#>  14  13  16  25  24 

colSums(m3)
#> 열1 열2 열3 
#>  35  28  29 
# apply()로 행 평균(MARGIN=1), 열 평균(MARGIN=2) 구하기
apply(m3, 1, mean)
#>      행1      행2      행3      행4      행5 
#> 4.666667 4.333333 5.333333 8.333333 8.000000 

apply(m3, 2, mean)
#> 열1 열2 열3 
#> 7.0 5.6 5.8 

행렬곱(%*%)은 왼쪽 행렬의 열 수와 오른쪽 행렬의 행 수가 같아야 계산할 수 있습니다.

mx <- matrix(1:9, nrow = 3)
my <- matrix(11:19, nrow = 3)
mx %*% my
#>      [,1] [,2] [,3]
#> [1,]  150  186  222
#> [2,]  186  231  276
#> [3,]  222  276  330

4.3 팩터 Factor

팩터(factor)는 "정해진 값(levels)만 허용하는 범주형(명목형) 자료 구조"입니다. 겉보기엔 문자열처럼 보이지만, 실제로는 정수 벡터로 저장되며 각 정수가 어떤 문자열(level)을 의미하는지는 별도의 levels 속성이 관리합니다.

성별 데이터 c("남성", "여성", "여성", "남성", "남성")을 팩터로 지정하면, R은 이 값이 "남성"과 "여성"이라는 두 가지 값(levels)만 가질 수 있다는 것을 기억합니다. 이후 실수로 "남정"을 입력하면 없는 범주값이라는 경고와 함께 결측값(NA)으로 처리되어, 데이터 입력 오류를 미리 걸러낼 수 있습니다.

팩터는 범주 사이에 순서가 없는 명목형(nominal) 팩터와 순서가 있는 순서형(ordinal) 팩터로 나뉩니다.

팩터 변수 만들기

factor()

factor(x, levels = , labels = , ordered = FALSE)는 벡터를 팩터로 변환합니다.

sex <- factor(c("남", "여", "여", "남", "남"))
sex
#> [1] 남 여 여 남 남
#> Levels: 남 여
# 팩터 변수의 Levels 보기
levels(sex)
#> [1] "남" "여"

levels 인자를 지정하지 않으면 R이 알아서 알파벳(가나다) 순으로 levels를 정합니다. levels의 순서를 직접 지정하고 싶다면 levels = 인자를 명시합니다.

sex2 <- factor(c("남", "여", "여", "남", "남"), levels = c("여", "남"))
sex2
#> [1] 남 여 여 남 남
#> Levels: 여 남

팩터는 벡터와 밀접한 관계입니다. 사실 팩터는 정수 벡터로 저장되는 데이터 구조입니다. 아래에서 보듯 levels는 문자로 저장되어 있지만, 각 요소는 그 문자가 levels 중 몇 번째인지를 가리키는 숫자로 저장되어 있습니다(sex에서는 1이 "남", 2가 "여").

str(sex)
#>  Factor w/ 2 levels "남","여": 1 2 2 1 1

순서형(ordered) 팩터 만들기

성별처럼 값 사이에 순서가 없는 자료도 있지만, "하·중·상"처럼 값 사이에 크기·순서가 있는 범주형 자료도 있습니다. 이런 경우 ordered = TRUE를 지정하면 대소 비교(<, >)까지 가능한 순서형 팩터가 됩니다.

grade <- factor(c("중", "상", "하", "중", "상"),
                levels = c("하", "중", "상"), ordered = TRUE)
grade
#> [1] 중 상 하 중 상
#> Levels: 하 < 중 < 상
# 순서형 팩터는 대소 비교가 가능함 (1번째 값 "중" > 3번째 값 "하")
grade[1] > grade[3]
#> [1] TRUE

일반 팩터(ordered = FALSE, 기본값)에서는 <, > 비교를 시도하면 경고와 함께 NA가 나오므로, 순서가 있는 범주형 자료라면 처음부터 ordered = TRUE로 만들어 두는 것이 안전합니다.

💡 문자열은 언제 팩터가 되는가 — stringsAsFactors의 역사: R 3.6 이하 버전에서는 data.frame()이나 read.csv()로 문자열 데이터를 불러오면 stringsAsFactors = TRUE가 기본값이어서 문자열 열이 자동으로 팩터로 바뀌었습니다. 이 동작이 종종 예상치 못한 문제(문자열 처리 함수가 팩터에 그대로 안 먹히는 등)를 일으켰기 때문에, R 4.0.0부터는 기본값이 stringsAsFactors = FALSE로 바뀌어 이제는 명시적으로 factor()를 호출하거나 stringsAsFactors = TRUE를 지정해야만 팩터가 됩니다. 즉 요즘 R에서는 "문자열은 기본적으로 문자열로 남고, 범주형으로 다루고 싶을 때만 명시적으로 팩터로 바꾼다"는 원칙이 표준입니다.

팩터 요소 추출·수정

팩터의 요소 추출은 벡터와 유사합니다.

sex[3]
#> [1] 여
#> Levels: 남 여

sex[c(1, 3)]
#> [1] 남 여
#> Levels: 남 여

sex[-1]
#> [1] 여 여 남 남
#> Levels: 남 여

팩터 요소의 수정도 벡터와 비슷하나, 기존 levels에 없는 값으로는 바꿀 수 없습니다.

sex[1] <- "여"
sex
#> [1] 여 여 여 남 남
#> Levels: 남 여
# levels에 없는 값을 넣으면 경고와 함께 NA가 됨
sex[1] <- "제3의성"
sex
#> 경고메시지(들):
#> `[<-.factor`(`*tmp*`, 1, value = "제3의성")에서:
#>   요인의 수준(factor level)이 올바르지 않아 NA가 생성되었습니다.
#> [1] <NA> 여   여   남   남  
#> Levels: 남 여

levels 범위를 벗어난 값을 추가하려면 먼저 새로운 levels 값을 추가해야 합니다.

levels(sex) <- c(levels(sex), "제3의성")
sex[1] <- "제3의성"
sex
#> [1] 제3의성 여      여      남      남     
#> Levels: 남 여 제3의성

droplevels()로 쓰지 않는 levels 정리하기

팩터에서 일부 값을 걸러내도, levels 목록에는 더 이상 존재하지 않는 범주가 그대로 남아 있는 경우가 많습니다. 예를 들어 "제3의성" 범주를 가진 데이터를 다 걸러냈는데도 table()이나 summary()에는 그 범주가 0건으로 계속 나타나면, 그래프의 범례나 통계표에 불필요한 항목이 계속 표시되는 문제가 생깁니다.

sub_sex <- sex[sex != "제3의성"]
table(sub_sex)
#> sub_sex
#>      남      여 제3의성 
#>       2       2       0 
levels(sub_sex)
#> [1] "남"      "여"      "제3의성"

droplevels(x)는 실제로 데이터에 등장하지 않는 levels를 목록에서 제거합니다.

sub_sex <- droplevels(sub_sex)
levels(sub_sex)
#> [1] "남" "여"

기타

팩터 형식 검사

is.factor(sex)
#> [1] TRUE

팩터 변환: factor() vs as.factor()

factor()와 as.factor()는 결과가 거의 같지만, 입력 데이터가 이미 팩터이거나 원자적 벡터(atomic vector)이면 as.factor()가 더 빠르게 동작하도록 최적화되어 있습니다. 새로운 levels 순서를 직접 지정하고 싶을 때는 factor(x, levels = ...)를, 단순히 벡터를 팩터로 바꾸기만 하면 될 때는 as.factor()를 쓰면 됩니다.

pf <- c(1, 5, 1, 3, 3, 2, 4, 2, 5, 1, 3, 5)
pf <- as.factor(pf)
pf
#>  [1] 1 5 1 3 3 2 4 2 5 1 3 5
#> Levels: 1 2 3 4 5

팩터 변수 요약

summary(sex2)
#> 여 남 
#>  2  3 

table(sex2)
#> sex2
#> 여 남 
#>  2  3 

summary()와 table()은 결과가 비슷해 보이지만, table()은 변수 이름을 결과 위에 표시(sex2)하고, 여러 팩터를 함께 넣어 교차표(cross table)를 만들 수 있다는 점이 다릅니다.

4.4 배열 Array

배열(array)은 "행렬을 층층이 쌓아 3차원 이상으로 확장한 것"입니다. 벡터가 1차원, 행렬이 2차원이라면, 배열은 2차원 이상(주로 3차원 이상)의 구조를 가리킵니다. 사실 행렬도 "2차원 배열"의 특수한 경우입니다. dim 속성에 몇 개의 숫자가 들어가는지가 차원 수를 결정합니다(1개면 벡터, 2개면 행렬과 동일, 3개 이상이면 고차원 배열).

배열 생성

array(data, dim = c(행, 열, 층, ...))로 배열을 만듭니다.

# 1~24의 숫자를 3행 4열, 2층짜리 배열로 만들기
arr1 <- array(1:24, dim = c(3, 4, 2))
arr1
#> , , 1
#> 
#>      [,1] [,2] [,3] [,4]
#> [1,]    1    4    7   10
#> [2,]    2    5    8   11
#> [3,]    3    6    9   12
#> 
#> , , 2
#> 
#>      [,1] [,2] [,3] [,4]
#> [1,]   13   16   19   22
#> [2,]   14   17   20   23
#> [3,]   15   18   21   24

배열 이름 붙이기

dimnames = list(행이름, 열이름, 층이름)으로 각 차원에 이름을 붙일 수 있습니다.

arr2 <- array(1:24, dim = c(4, 3, 2),
              dimnames = list(c("일", "이", "삼", "사"),
                              c("가", "나", "다"),
                              c("층1", "층2")))
arr2
#> , , 층1
#> 
#>    가 나 다
#> 일  1  5  9
#> 이  2  6 10
#> 삼  3  7 11
#> 사  4  8 12
#> 
#> , , 층2
#> 
#>    가 나 다
#> 일 13 17 21
#> 이 14 18 22
#> 삼 15 19 23
#> 사 16 20 24

배열의 요소 추출·수정

특정 위치에 있는 요소 추출

# 2행, 2열, 2층 요소
arr2[2, 2, 2]
#> [1] 18

# 각 층의 2행
arr2[2, , ]
#>    층1 층2
#> 가   2  14
#> 나   6  18
#> 다  10  22

# 각 층의 3행 3열 요소
arr2[3, 3, ]
#> 층1 층2 
#>  11  23 

# 2층의 행렬 전체
arr2[, , 2]
#>    가 나 다
#> 일 13 17 21
#> 이 14 18 22
#> 삼 15 19 23
#> 사 16 20 24

이름으로 요소 추출

# 2행(이), 2열(나), 2층(층2) 요소
arr2["이", "나", "층2"]
#> [1] 18

# 각 층의 2행(이)
arr2["이", , ]
#>    층1 층2
#> 가   2  14
#> 나   6  18
#> 다  10  22

마이너스 기호(-)를 이용하여 요소 제외

arr2[-2, -2, ]
#> , , 층1
#> 
#>    가 다
#> 일  1  9
#> 삼  3 11
#> 사  4 12
#> 
#> , , 층2
#> 
#>    가 다
#> 일 13 21
#> 삼 15 23
#> 사 16 24

조건으로 요소 추출

arr2[arr2 > 11]
#>  [1] 12 13 14 15 16 17 18 19 20 21 22 23 24

arr2[arr2 %% 2 == 0]
#>  [1]  2  4  6  8 10 12 14 16 18 20 22 24

배열 요소 수정

arr2[2, 2, 1] <- 55
arr2[, , 1]
#>    가 나 다
#> 일  1  5  9
#> 이  2 55 10
#> 삼  3  7 11
#> 사  4  8 12

배열 연산

arr2 * 2
#> , , 층1
#> 
#>    가  나 다
#> 일  2  10 18
#> 이  4 110 20
#> 삼  6  14 22
#> 사  8  16 24
#> 
#> , , 층2
#> 
#>    가 나 다
#> 일 26 34 42
#> 이 28 36 44
#> 삼 30 38 46
#> 사 32 40 48

차원이 같은 배열끼리는 원소별 연산이 가능합니다.

ax <- array(1:12, dim = c(2, 3, 2))
ay <- array(11:22, dim = c(2, 3, 2))
ax + ay
#> , , 1
#> 
#>      [,1] [,2] [,3]
#> [1,]   12   16   20
#> [2,]   14   18   22
#> 
#> , , 2
#> 
#>      [,1] [,2] [,3]
#> [1,]   24   28   32
#> [2,]   26   30   34

4.5 데이터 프레임 Data frame

데이터 프레임(data frame)은 "길이가 같은 여러 개의 벡터를 열로 삼아 묶은 리스트"입니다. 겉모습은 행렬(표)과 비슷하지만, 내부 구조는 리스트에 가깝습니다.

데이터 프레임은 행(케이스)과 열(변수)로 구성된 2차원 데이터 구조로, 열마다 서로 다른 자료형을 가질 수 있다는 점에서 행렬과 다르고, 모든 열의 길이가 같아야 한다는 점에서 일반 리스트와 다릅니다. 실제로 is.list(df)는 TRUE를 돌려주며, 이는 데이터 프레임이 리스트 위에 만들어진 특수한 구조임을 보여줍니다. R에서 통계분석에 가장 많이 사용되는 자료구조입니다.

데이터 프레임 만들기

데이터 프레임을 만드는 가장 흔한 방법은 외부 파일(CSV 등)에서 정리된 데이터를 읽어 들이는 것입니다. 이 방법은 입출력 함수에서 자세히 다룹니다. 여기서는 이미 있는 벡터들을 data.frame()으로 묶는 방법으로 데이터 프레임을 만들어 보겠습니다.

name  <- c("김민준", "이서연", "박도윤", "최지우", "정하은")
age   <- c(24, 27, 22, 29, 25)
sex   <- c("남", "여", "남", "여", "여")
score <- c(88, 95, 76, 82, 91)

df <- data.frame(name, age, sex, score)
df
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  27  여    95
#> 3 박도윤  22  남    76
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91

💡 문자열 열은 더 이상 자동으로 팩터가 되지 않습니다: 위 df의 name, sex 열은 문자형(character) 그대로 남아 있습니다. R 3.6 이하였다면 별다른 옵션 없이 data.frame()을 호출해도 문자열 열이 자동으로 팩터로 바뀌었지만(), R 4.0.0부터는 기본값이 바뀌어 명시적으로 stringsAsFactors = TRUE를 지정해야만 팩터가 됩니다.

class(df$name)
old_df <- data.frame(name, stringsAsFactors = TRUE)
class(old_df$name)
#> [1] "character"
#> [1] "factor"

성별처럼 범주형으로 다루고 싶은 열이 있다면, data.frame()에만 맡기지 말고 sex = factor(sex)처럼 그 열만 직접 팩터로 지정하는 것이 요즘 R 스타일에 맞습니다.

🔎 참고: tidyverse의 tibble은 data.frame을 확장한 자료구조로 출력 형식이나 일부 동작이 다르지만, 이 매뉴얼은 Base R 범위이므로 tibble은 다루지 않습니다.

데이터 프레임 구조·요약 보기

str(df)
#> 'data.frame':    5 obs. of  4 variables:
#>  $ name : chr  "김민준" "이서연" "박도윤" "최지우" ...
#>  $ age  : num  24 27 22 29 25
#>  $ sex  : chr  "남" "여" "남" "여" ...
#>  $ score: num  88 95 76 82 91
nrow(df)   # 행 개수(케이스 수)
#> [1] 5

ncol(df)   # 열 개수(변수 수)
#> [1] 4

dim(df)    # 행과 열의 개수
#> [1] 5 4

head()·tail()은 데이터 프레임의 앞부분·뒷부분 몇 행만 미리 살펴볼 때 유용합니다. 특히 행이 수천~수백만 개인 실제 데이터를 다룰 때는 df 전체를 출력하는 대신 이 함수들로 먼저 형태를 확인하는 것이 일반적인 관행입니다.

head(df, 3)   # 앞 3행
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  27  여    95
#> 3 박도윤  22  남    76

tail(df, 2)   # 뒤 2행
#>     name age sex score
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91
summary(df)
#>      name                age           sex                score     
#>  Length:5           Min.   :22.0   Length:5           Min.   :76.0  
#>  Class :character   1st Qu.:24.0   Class :character   1st Qu.:82.0  
#>  Mode  :character   Median :25.0   Mode  :character   Median :88.0  
#>                     Mean   :25.4                      Mean   :86.4  
#>                     3rd Qu.:27.0                      3rd Qu.:91.0  
#>                     Max.   :29.0                      Max.   :95.0  

데이터 추출·수정

특정 위치(행과 열 기준)에 있는 데이터 추출하기

# name 열(변수) 추출 : 데이터프레임과 열 이름 사이에 $ 기호 사용
df$name
#> [1] "김민준" "이서연" "박도윤" "최지우" "정하은"

# df$name과 동일
df[, "name"]
#> [1] "김민준" "이서연" "박도윤" "최지우" "정하은"

# 첫번째 열
df[, 1]
#> [1] "김민준" "이서연" "박도윤" "최지우" "정하은"
# 2~3번째 행
df[2:3, ]
#>     name age sex score
#> 2 이서연  27  여    95
#> 3 박도윤  22  남    76

# name 열의 3번째 값
df$name[3]
#> [1] "박도윤"
# 연령 평균 (na.rm = TRUE는 결측값을 제외하고 계산)
mean(df$age, na.rm = TRUE)
#> [1] 25.4

마이너스 기호(-)를 이용하여 데이터 제외

# 3번째 열(sex) 제외
df[, -3]
#>     name age score
#> 1 김민준  24    88
#> 2 이서연  27    95
#> 3 박도윤  22    76
#> 4 최지우  29    82
#> 5 정하은  25    91

조건으로 데이터 추출

데이터가 많을 때는 대괄호 안에 조건식을 직접 입력해 추출하는 방식이 subset()을 쓰는 것보다 계산이 더 빠릅니다.

# score가 90 이상인 데이터
df[df$score >= 90, ]
#>     name age sex score
#> 2 이서연  27  여    95
#> 5 정하은  25  여    91

# score가 90 이상인 데이터 중 name, age만
df[df$score >= 90, c("name", "age")]
#>     name age
#> 2 이서연  27
#> 5 정하은  25

subset()을 쓰면 조건식과 열 선택을 조금 더 자연스러운 문장처럼 쓸 수 있습니다.

subset(df, score >= 90)
#>     name age sex score
#> 2 이서연  27  여    95
#> 5 정하은  25  여    91

subset(df, score >= 90, select = c(name, age))
#>     name age
#> 2 이서연  27
#> 5 정하은  25

⚠️ subset()은 대화형 분석에는 편리하지만 함수 안에서 프로그래밍적으로 쓰기에는 주의가 필요합니다(?subset 참고).

데이터 수정

# 2번째 행 이서연의 age를 28로 수정
df$age[2] <- 28
# 3행 박도윤의 score를 90으로 수정
df[3, "score"] <- 90
df
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  28  여    95
#> 3 박도윤  22  남    90
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91

데이터 프레임 구조 수정

행 추가

새 행을 데이터 프레임 형태로 만들어 rbind()로 이어 붙이는 방법이 가장 안전합니다(열 이름과 타입이 자동으로 맞춰집니다).

new_row <- data.frame(name = "홍길동", age = 26, sex = "남", score = 85)
df <- rbind(df, new_row)
df
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  28  여    95
#> 3 박도윤  22  남    90
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91
#> 6 홍길동  26  남    85

열 추가

df$새열 <- 값처럼 $에 새 이름을 대입하면 열이 바로 추가됩니다.

df$grade <- ifelse(df$score >= 90, "A", "B")
df
#>     name age sex score grade
#> 1 김민준  24  남    88     B
#> 2 이서연  28  여    95     A
#> 3 박도윤  22  남    90     A
#> 4 최지우  29  여    82     B
#> 5 정하은  25  여    91     A
#> 6 홍길동  26  남    85     B

새 데이터가 이미 벡터·데이터프레임으로 준비되어 있을 때 cbind()를 활용 할 수 있습니다.

행 삭제

# name이 "홍길동"이 아닌 행만 남기기
df <- df[df$name != "홍길동", ]
df
#>     name age sex score grade
#> 1 김민준  24  남    88     B
#> 2 이서연  28  여    95     A
#> 3 박도윤  22  남    90     A
#> 4 최지우  29  여    82     B
#> 5 정하은  25  여    91     A

열 삭제

df$열이름 <- NULL로 특정 열을 통째로 없앨 수 있습니다.

df$grade <- NULL
df
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  28  여    95
#> 3 박도윤  22  남    90
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91

기타

# 열 이름 추출
names(df)
#> [1] "name"  "age"   "sex"   "score"
# 열 개수 (ncol과 length는 데이터 프레임에서 동일하게 동작함)
ncol(df)
#> [1] 4

length(df)
#> [1] 4
nrow(df)
#> [1] 5

숫자형 열들에 대해서는 colMeans(), colSums(), rowMeans(), rowSums()를 그대로 쓸 수 있습니다. 단, 문자·팩터 열이 섞여 있으면 미리 숫자형 열만 골라내야 합니다.

colMeans(df[, c("age", "score")])
#>   age score 
#>  25.6  89.2 

4.6 리스트 List

리스트(list)는 "서로 다른 구조·타입의 객체들을 하나로 묶은 것"입니다. 기차 칸을 이어 붙이듯, 각 칸(요소)에 완전히 다른 내용물을 담을 수 있고, 심지어 리스트 안에 또 다른 리스트를 담아 중첩 구조를 만들 수도 있습니다. R의 여러 복합 객체(데이터 프레임, 회귀분석 결과 등)가 내부적으로 리스트 위에 구현되어 있습니다.

리스트 만들기

list(이름1 = 값1, 이름2 = 값2, ...)로 서로 다른 자료구조들을 하나로 묶습니다.

numvec <- seq(1, 9, by = 2)
strvec <- c("서울", "부산", "대구")
mtx <- matrix(1:6, nrow = 2)

mylist <- list(numbers = numvec, cities = strvec, mat = mtx, students = df)
mylist
#> $numbers
#> [1] 1 3 5 7 9
#> 
#> $cities
#> [1] "서울" "부산" "대구"
#> 
#> $mat
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
#> 
#> $students
#>     name age sex score
#> 1 김민준  24  남    88
#> 2 이서연  28  여    95
#> 3 박도윤  22  남    90
#> 4 최지우  29  여    82
#> 5 정하은  25  여    91

이름을 지정하지 않고 list(numvec, strvec, mtx, df)처럼 만들 수도 있지만, 이후 [[1]], [[2]]처럼 번호로만 접근해야 해서 코드를 알아보기 어렵습니다. 요소마다 이름을 지정해 두면 mylist$numbers처럼 의미가 드러나는 방식으로 접근할 수 있어 권장됩니다.

리스트 구조 보기

str(mylist)
#> List of 4
#>  $ numbers : num [1:5] 1 3 5 7 9
#>  $ cities  : chr [1:3] "서울" "부산" "대구"
#>  $ mat     : int [1:2, 1:3] 1 2 3 4 5 6
#>  $ students:'data.frame':    5 obs. of  4 variables:
#>   ..$ name : chr [1:5] "김민준" "이서연" "박도윤" "최지우" ...
#>   ..$ age  : num [1:5] 24 28 22 29 25
#>   ..$ sex  : chr [1:5] "남" "여" "남" "여" ...
#>   ..$ score: num [1:5] 88 95 90 82 91
# 리스트에 있는 요소의 개수
length(mylist)
#> [1] 4

# 리스트 요소의 이름
names(mylist)
#> [1] "numbers"  "cities"   "mat"      "students"

리스트에서 데이터 추출·수정

특정 위치에 있는 데이터 추출하기

$, [[ ]], [ ] 세 가지 방식은 결과가 다릅니다. [ ]는 "리스트의 부분 리스트"를 돌려주지만(타입은 list), [[ ]]와 $는 그 요소의 실제 내용물을 그대로 돌려줍니다(타입은 내용물 자체의 타입).

# $ : 리스트와 요소 이름 사이에 $ 기호 사용
mylist$mat
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

# [[ ]] : 이름 또는 위치 번호로 접근 가능, 결과는 동일
mylist[["mat"]]
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
# [ ] 는 리스트의 하위 요소를 "리스트째로" 반환 (타입 list)
typeof(mylist["mat"])
#> [1] "list"

# [[ ]] 는 하위 요소의 내용물을 그대로 반환 (타입 integer)
typeof(mylist[["mat"]])
#> [1] "integer"

중첩된 데이터 추출

# cities 요소의 2번째 값
mylist$cities[2]
#> [1] "부산"

# students(데이터 프레임) 요소의 name 열
mylist$students$name
#> [1] "김민준" "이서연" "박도윤" "최지우" "정하은"

리스트의 데이터 수정

mylist$cities[2] <- "인천"
mylist$cities
#> [1] "서울" "인천" "대구"

리스트 구조 수정

리스트 요소 추가·제거

# 요소 추가
mylist$new_item <- 1:3
names(mylist)
#> [1] "numbers"  "cities"   "mat"      "students" "new_item"
# NULL 대입으로 요소 제거 (데이터 프레임 열 삭제와 같은 원리, 2.5.3 참고)
mylist$new_item <- NULL
names(mylist)
#> [1] "numbers"  "cities"   "mat"      "students"

리스트 병합

c()로 리스트 두 개를 이어 붙일 수 있습니다.

list1 <- list(x1 = 1:3, x2 = c("a", "b"))
list2 <- list(y1 = 10:12)
c(list1, list2)
#> $x1
#> [1] 1 2 3
#> 
#> $x2
#> [1] "a" "b"
#> 
#> $y1
#> [1] 10 11 12

리스트를 벡터로 변환

unlist(x)는 리스트의 모든 요소를 하나의(단일 타입) 벡터로 풀어냅니다. 요소들의 타입이 서로 다르면 모두 가장 포괄적인 타입(보통 문자형)으로 강제 변환됩니다.

unlist(list1)
#> x11 x12 x13 x21 x22 
#> "1" "2" "3" "a" "b" 

위 결과에서 보듯, list1의 두 요소(x1은 숫자, x2는 문자)가 하나의 문자형 벡터로 합쳐졌고, 이름도 원래 요소 이름에 위치 번호가 붙은 형태(x11, x12, ...)로 자동 생성되었습니다.